Add store support for Spin adapter (KV, config, secret)#253
Conversation
- dispatch() in request.rs now injects ConfigStoreHandle, KvHandle, and SecretHandle into request extensions on every request - SpinSecretStore normalises the lookup key to lowercase so conventional uppercase names (e.g. SMOKE_SECRET) resolve to the correct Spin variable - contract.rs gains store injection smoke tests (config, kv, secret) and wasm32 compile-time trait checks for SpinKvStore and SpinSecretStore
- spin.toml gains key_value_stores = ["default"] binding and variables declarations for greeting and smoke_secret - edgezero.toml adds "spin" to adapters for config, kv, and secrets routes - smoke_test_kv/config/secrets.sh each gain a spin case that builds the WASM binary and starts spin up --listen 127.0.0.1:3000; the config script skips dotted-key checks (Spin variable names cannot contain dots); the secrets script passes SPIN_VARIABLE_SMOKE_SECRET at startup
spin up creates .spin/ (SQLite KV database and component logs) in the adapter directory during local development, mirroring .wrangler/ for CF.
instead of silently truncating; callers now get an explicit signal rather than incomplete pagination results - Correct DEFAULT_MAX_LIST_KEYS, with_max_list_keys, and module-level docs to accurately describe error-return behaviour (not truncation, not "unbounded allocation" guarding) - Add log::debug in SpinSecretStore::get_bytes when store_name is non-empty so callers learn the flat-namespace constraint at runtime - Add comment in config_store contract tests explaining the InMemory backend accepts dotted/uppercase keys that the real Spin backend would reject via InvalidName - Add comment in lib.rs explaining why SpinConfigStore has different feature gating than SpinKvStore and SpinSecretStore
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Summary
Thanks for adding Spin store support and the smoke/CI coverage. CI is green. I found two configuration-semantics issues worth addressing before relying on non-default store names: Spin KV dispatch is hard-coded to default, and accepting a Spin config-store adapter override is misleading because the runtime does not consume that name.
Resolve conflict in test.yml: keep matrix-based wasm test job (cloudflare/fastly/spin) from this branch; drop main's per-adapter split jobs which pre-date Spin support.
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Summary
Reviewed the Spin store support changes. The implementation adds useful store coverage and CI/smoke-test expansion, but a few issues need attention before merge: one wasm no-default feature build currently fails, Spin store dispatch bypasses manifest resolution, and KV TTL/listing behavior does not match the core contract.
Cross-cutting finding
- 📝 Document Spin-specific store behavior: The user docs and secret-store rustdoc still describe KV TTL, config store behavior, and named secret stores as if Spin behaves like the other adapters. After the implementation behavior is finalized, please update
docs/guide/kv.md,docs/guide/configuration.md, andcrates/edgezero-core/src/secret_store.rsto cover Spin's KV limitations, component variables/flat namespace, lowercase variable names, and whetherrun_apphonors manifest gating.
😃 Praise
- 😃 The Spin wasm CI matrix entry plus the new smoke-test coverage are a strong addition and should catch real integration regressions across the adapter.
CI Status
cargo fmt --all -- --check: PASScargo clippy --workspace --all-targets --all-features -- -D warnings: PASScargo test --workspace --all-targets: PASScargo check --workspace --all-targets --features "fastly cloudflare spin": PASScargo check -p edgezero-adapter-spin --target wasm32-wasip1 --features spin: PASS- Additional check:
cargo check -p edgezero-adapter-spin --target wasm32-wasip1 --no-default-features: FAIL (see inline comment onSpinConfigStore)
| match &self.inner { | ||
| #[cfg(target_arch = "wasm32")] | ||
| SpinConfigBackend::Spin => { | ||
| use spin_sdk::variables; |
There was a problem hiding this comment.
🔧 Gate SpinConfigStore on the spin feature: spin-sdk is an optional dependency enabled only by the spin feature, but this wasm32 arm is gated only on target_arch = "wasm32". As a result, cargo check -p edgezero-adapter-spin --target wasm32-wasip1 --no-default-features currently fails here with unresolved import spin_sdk.
Suggestion: gate the production backend consistently on both cfgs, and update the fallback branch to match:
#[cfg(all(feature = "spin", target_arch = "wasm32"))]
Spin,
#[cfg(all(feature = "spin", target_arch = "wasm32"))]
pub fn new() -> Self { ... }
#[cfg(all(feature = "spin", target_arch = "wasm32"))]
SpinConfigBackend::Spin => { ... }| /// `edgezero.toml` is set to a custom value). | ||
| pub async fn dispatch(app: &App, req: IncomingRequest) -> anyhow::Result<spin_sdk::http::Response> { | ||
| let core_request = into_core_request(req).await?; | ||
| dispatch_with_kv_label(app, req, "default").await |
There was a problem hiding this comment.
🔧 Resolve Spin store handles from the manifest: run_app reaches this path, so Spin always opens the hard-coded "default" KV label and always injects config/secrets. That bypasses [stores.kv].name, [stores.kv.adapters.spin], [stores.secrets].enabled, and whether [stores.config] exists, unlike the Fastly and Cloudflare run_app(manifest_src, ...) paths. A user setting [stores.kv] name = "MY_KV" would still get no KV handle unless they manually avoid run_app and call dispatch_with_kv_label themselves.
Suggestion: add a manifest-aware Spin entry point that resolves manifest.kv_store_name(edgezero_core::app::SPIN_ADAPTER), manifest.secret_store_enabled(...), and config-store presence before dispatching, then update the Spin template to pass the embedded edgezero.toml manifest.
| value: Bytes, | ||
| _ttl: Duration, | ||
| ) -> Result<(), KvError> { | ||
| log::warn!("SpinKvStore: TTL is not supported by the Spin KV API; storing without expiry"); |
There was a problem hiding this comment.
🔧 Do not report TTL writes as successful when Spin cannot expire them: put_bytes_with_ttl logs a warning but then writes the value without any expiry and returns Ok(()). The core KV contract says TTL values should be treated as expired after the TTL, so callers can accidentally retain temporary/session data indefinitely without any error signal.
Suggestion: return an explicit validation/unsupported error instead of writing when TTL cannot be honored:
return Err(KvError::Validation(
"Spin KV does not support TTL; use put_bytes for non-expiring values".to_string(),
));| cursor: Option<&str>, | ||
| limit: usize, | ||
| ) -> Result<KvPage, KvError> { | ||
| let all_keys = self |
There was a problem hiding this comment.
🔧 Apply bounded-listing semantics before fetching keys: Store::get_keys() materializes the entire Spin KV store before max_list_keys is checked, so the cap does not actually prevent unbounded allocation in the request path. This also conflicts with the core KvStore::list_keys_page contract, which says implementations should keep memory bounded to a single page of keys.
Suggestion: unless Spin exposes prefix/server-side pagination, treat key listing as unsupported for this adapter rather than fetching the whole store:
Err(KvError::Validation(
"Spin KV key listing is unsupported because Store::get_keys() is unbounded".to_string(),
))| pub name: Option<String>, | ||
| /// Per-adapter name overrides, keyed by supported lowercase adapter name | ||
| /// (`axum`, `cloudflare`, or `fastly`). | ||
| /// (`axum`, `cloudflare`, `fastly`, or `spin`). |
There was a problem hiding this comment.
🔧 Keep the config-store adapter docs aligned with validation: this rustdoc now says [stores.config.adapters] can be keyed by spin, but SUPPORTED_CONFIG_STORE_ADAPTERS intentionally excludes spin and the new test expects [stores.config.adapters.spin] to fail validation. That will mislead users into writing a manifest shape that EdgeZero rejects.
Suggestion: remove spin from this list and call out the reason, e.g. Spin config uses component variables in a flat namespace, so stores.config.adapters.spin is rejected.
Summary
SpinKvStore,SpinConfigStore, andSpinSecretStoreso Spin handlers can access key-value, configuration, and secret data through the samectx.kv_handle(),ctx.config_store(), andctx.secret_handle()API as every other adapter.spin-adapter-testsCI job, store injection contract tests, and Spin support to all three smoke test scripts.Changes
crates/edgezero-adapter-spin/src/config_store.rsSpinConfigStorebacked byspin_sdk::variables; dual-backend (Spin / in-memory for tests); contract tests via macrocrates/edgezero-adapter-spin/src/key_value_store.rsSpinKvStorebacked byspin_sdk::key_value; in-process prefix filter + pagination; configurablemax_list_keyscap (warn + truncate); TTL silently ignoredcrates/edgezero-adapter-spin/src/secret_store.rsSpinSecretStorebacked byspin_sdk::variables; key normalised to lowercase to match Spin variable naming rulescrates/edgezero-adapter-spin/src/request.rsdispatch()now injectsConfigStoreHandle,KvHandle, andSecretHandleinto every request's extensionscrates/edgezero-adapter-spin/src/lib.rscrates/edgezero-adapter-spin/tests/contract.rsSpinKvStoreandSpinSecretStorecrates/edgezero-core/src/manifest.rs"spin"toSUPPORTED_CONFIG_STORE_ADAPTERScrates/edgezero-core/src/config_store.rsConfigStoretrait doc to listSpinConfigStore.github/workflows/test.ymlspin-adapter-testsjob: native tests + wasm32-wasip1 compilation checkexamples/app-demo/crates/app-demo-adapter-spin/spin.tomlkey_value_stores = ["default"]binding and variable declarations forgreetingandsmoke_secretexamples/app-demo/edgezero.toml"spin"toadaptersfor config, KV, and secrets routesscripts/smoke_test_kv.shspincasescripts/smoke_test_config.shspincase; skips dotted-key checks (Spin variable names cannot contain dots)scripts/smoke_test_secrets.shspincase; passes secret value viaSPIN_VARIABLE_SMOKE_SECRETat startup.gitignore.spin/(runtime SQLite KV database and component logs)Closes
Closes #73
Closes #74
Test plan
cargo test --workspace --all-targetscargo clippy --workspace --all-targets --all-features -- -D warningscargo check --workspace --all-targets --features "fastly cloudflare spin"cargo check -p edgezero-adapter-spin --target wasm32-wasip1 --features spin./scripts/smoke_test_kv.sh spin./scripts/smoke_test_config.sh spin./scripts/smoke_test_secrets.sh spinChecklist
{id}syntax (not:id)edgezero_core(nothttpcrate)